/*
**************************************************************************************
*Myriad Engine                                                                       *
*Copyright (C) 2006-2007, 5d Studios (www.5d-Studios.com)                            *
*                                                                                    *
*This library is free software; you can redistribute it and/or                       *
*modify it under the terms of the GNU Lesser General Public                          *
*License as published by the Free Software Foundation; either                        *
*version 2.1 of the License, or (at your option) any later version.                  *
*                                                                                    *
*This library is distributed in the hope that it will be useful,                     *
*but WITHOUT ANY WARRANTY; without even the implied warranty of                      *
*MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU                   *
*Lesser General Public License for more details.                                     *
*                                                                                    *
*You should have received a copy of the GNU Lesser General Public                    *
*License along with this library; if not, write to the Free Software                 *
*Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA      *
**************************************************************************************
*/
package leola.utils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

import leola.backend.EvalException;
import leola.backend.interpreter.LeoTypeConverter;
import leola.backend.interpreter.ObjectArrayJavaTypeVisitor;
import leola.types.LeoNativeClass;
import leola.types.LeoNull;
import leola.types.LeoObject;

/**
 * @author Tony
 *
 */
public class ClassUtil {
	
	/**
	 * Retrieves a method by name (grabs the first if overloaded).
	 * 
	 * @param methodName
	 * @return the method if found, otherwise null
	 */
	public static Method getMethodByName(Class<?> aClass, String methodName, Class<?> ... params) {
		Method result = null;
		try {
			result = aClass.getMethod(methodName, params);
		}
		catch(Exception e) {			
		}
		
		return (result);
	}
	
	/**
	 * Invokes a method reflectively.
	 * 
	 * @param method
	 * @param owner
	 * @param params
	 * @return
	 * @throws Exception
	 */
	public static Object invokeMethod(Method method, Object owner, LeoObject[] params) throws Exception {
		Object result = null;						
		
		ObjectArrayJavaTypeVisitor visitor = new ObjectArrayJavaTypeVisitor();
		try { 
			method.setAccessible(true);
			result = tryMethod(visitor, owner, method, params);
		}
		catch(InvocationTargetException e) {
			/* This was a legitimate method invokation, so
			 * lets bomb out
			 */
			throw new Exception(e.getCause());
		}
		catch(Throwable e) {
			/* try other methods */
			// System.out.println(e);
		}
		
		return result;
	}
	
	/**
	 * Invokes a method reflectively.
	 * 
	 * @param method
	 * @param owner
	 * @param params
	 * @return
	 * @throws Exception
	 */
	public static Object invokeMethod(Method method, Object owner, Object[] params) throws Exception {
		Object result = null;						
		
		try { 
			method.setAccessible(true);
			result = method.invoke(owner, params);
		}
		catch(InvocationTargetException e) {
			/* This was a legitimate method invokation, so
			 * lets bomb out
			 */
			throw new Exception(e.getCause());
		}
		catch(Throwable e) {
			/* try other methods */
			// System.out.println(e);
		}
		
		return result;
	}
	
	/**
	 * @param aClass
	 * @param fieldName
	 * @return
	 */
	public static LeoObject getFieldValue(LeoNativeClass aClass, String fieldName) {
		LeoObject result = null;
		try {
			Class<?> ownerClass = aClass.getNativeClass();
			Field field = ownerClass.getField(fieldName);
			Object javaObj = field.get(aClass.getInstance());
			result = LeoTypeConverter.convertToLeolaType(javaObj);
		} catch (Exception e) {			
		}
		
		return result;
	}
	
	
	
	/**
	 * Finds the best possible method. If none is found suitable an exception is thrown.
	 * 
	 * @param aClass
	 * @param methodName
	 * @param paramTypes
	 * @return a {@link Pair} 
	 * @throws Exception
	 */
	public static Pair<Method, Object> invokeMethod(Class<?> aClass, String methodName, Object owner, Object[] params) throws Exception {
		Pair<Method, Object> result = null;
		boolean methodFound = false;
		
		List<Method> methods = getAllDeclaredMethods(aClass);
				
		for(Method m: methods) {
			if(m.getName().equals(methodName)) {
				try {
					m.setAccessible(true);
					Object methodResult = m.invoke(owner, params);
					result = new Pair<Method, Object>(m, methodResult);
					methodFound = true;
					break;
				}
				catch(InvocationTargetException e) {
					/* This was a legitimate method invokation, so
					 * lets bomb out
					 */
					throw new Exception(e.getCause());
				}
				catch(Throwable e) {
					/* try other methods */
					// System.out.println(e);
				}
			}
		}
		
		if ( ! methodFound ) {
			throw new EvalException("No method defined for: " + owner + " methodName: " + methodName);
		}
		
		return result;
		
	}
	
	
	/**
	 * Finds the best possible method. If none is found suitable an exception is thrown.
	 * 
	 * @param aClass
	 * @param methodName
	 * @param paramTypes
	 * @return a {@link Pair} 
	 * @throws Exception
	 */
	public static Pair<Method, Object> invokeMethod(Class<?> aClass, String methodName, Object owner, LeoObject[] params) throws Exception {
		Pair<Method, Object> result = null;
		boolean methodFound = false;
		
		List<Method> methods = getAllDeclaredMethods(aClass);
		
		ObjectArrayJavaTypeVisitor visitor = new ObjectArrayJavaTypeVisitor();
		for(Method m: methods) {
			if(m.getName().equals(methodName)) {
				try {
					m.setAccessible(true);
					Object methodResult = tryMethod(visitor, owner, m, params);
					result = new Pair<Method, Object>(m, methodResult);
					methodFound = true;
					break;
				}
				catch(InvocationTargetException e) {
					/* This was a legitimate method invokation, so
					 * lets bomb out
					 */
					throw new Exception(e.getCause());
				}
				catch(Throwable e) {
					/* try other methods */
					// System.out.println(e);
				}
			}
		}
		
		if ( ! methodFound ) {
			throw new EvalException("No method defined for: " + owner + " methodName: " + methodName);
		}
		
		return result;
		
	}
	
	/**
	 * Attempts to instantiate the object
	 * @param constructor
	 * @param params
	 * @param paramTypes
	 * @return
	 */
	private static Object tryMethod(ObjectArrayJavaTypeVisitor visitor, Object owner, Method method, LeoObject[] params) 
		throws InvocationTargetException, Exception {
		
		Class<?>[] paramTypes = method.getParameterTypes();
		if ( paramTypes.length != params.length ) {
			throw new Exception();
		}
		
		Object[] args = new Object[paramTypes.length];
		visitor.params = args;
		
		for(int i = 0; i < paramTypes.length; i++ ) {
			visitor.currentIndex = i;
			
			Class<?> aCl = paramTypes[i];
			
			/* Leola allows for missing arguments */
			LeoObject arg = LeoNull.LEONULL;
			if ( i < params.length ) {
				arg = params[i];
			}
			
			LeoTypeConverter.convertLeoObjectToJavaObj(visitor, aCl, arg);
		}
		
		Object result = method.invoke(owner, args);
		
		
		return (result);
	}
		
	/**
	 * Is of type
	 */
	public static boolean isType(Class<?> type, Class<?> ...classes ) {
		boolean result = false;
		for(Class<?> c: classes) {
			result = result || type.equals(c);
			if ( result ) {
				break;
			}
		}
		return result;
	}
	
	/**
	 * Determines if the supplied child class inherits from the supplied parent class.
	 * @param child
	 * @param parent
	 * @return true if child inherits from parent, false otherwise
	 */
	public static boolean inheritsFrom(Class<?> child, Class<?> parent) {
		for(Class<?> a = child; a != null; a = a.getSuperclass()) {
			if ( a.equals(parent) ) {
				return true;
			}
		}
		
		return false;
	}
	
		
	/**
	 * Gets all the methods even from the parent class.
	 * @param aClass
	 * @return
	 */
	public static List<Method> getAllDeclaredMethods(Class<?> aClass) {
		List<Method> methods = new ArrayList<Method>();
		try {
			for(Class<?> a = aClass; a != null && !a.getClass().equals(Object.class); 
			    a = a.getSuperclass()) {
				
				for(Method m : a.getDeclaredMethods()) {
					/* Don't grab private members */
					if( (m.getModifiers() & Modifier.PRIVATE) == 0 ) {
						methods.add(m);
					}
				}
			}
		}
		catch(Exception e) {
			/* ignore */
		}
		
		return (methods);
	}
	
	/**
	 * Get the inherited method.
	 * @param aClass
	 * @param method
	 * @return
	 */
	public static Method getInheritedMethod(Class<?> aClass, Method method) {
		Method inheritedMethod = null;
		try {
			for(Class<?> a = aClass; a != null; a = a.getSuperclass()) {
				inheritedMethod = aClass.getMethod(method.getName(), method.getParameterTypes());
				if ( inheritedMethod != null ) {
					return inheritedMethod;
				}
			}
		}
		catch (Exception e) {
			/* Ignore */
		}
		
		return null;
	}
	
	/**
	 * Retrieves the annotation from the method, if it is not on the method, the parent
	 * is checked.  This solves the "inherited annotation" problem for interfaces.
	 * @param ownerClass
	 * @param method
	 * @return
	 */
	public static <T extends Annotation> T getAnnotation(Class<T> annotation, Class<?> ownerClass, Method method) {
		if ( method == null ) {
			return null;
		}
		
		if ( method.isAnnotationPresent(annotation) ) {
			return method.getAnnotation(annotation);
		}
		else {
			for(Class<?> aInterface : ownerClass.getInterfaces()) {
				Method inheritedMethod = getInheritedMethod(aInterface, method);
				if ( inheritedMethod != null ) {
					T result = getAnnotation(annotation, aInterface, inheritedMethod);
					if ( result != null ) {
						return result;
					}
				}
			}
			
			/* Query the parent class for the annotation */
			Class<?> superClass = method.getDeclaringClass().getSuperclass();
			Method inheritedMethod = getInheritedMethod(superClass, method);
			
			return getAnnotation(annotation, superClass, inheritedMethod);
		}
	}
	
}
